﻿
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Xunit;
using Xunit.Extensions;

namespace EmperialApps.WeatherSpark.Data {

    using RawDataValues = ReadOnlyCollection<double>;

    public class TestForecastExtensions {

        private static readonly Forecast DefaultForecast = new Forecast(
            new Coordinate( 30.27, -97.74 ), "Austin TX",
            new DateTimeOffset( 2012, 02, 19, 7, 0, 0, 0, TimeSpan.FromHours( -6 ) ),
            new RawDataValues( new[] {
                                                     5.6,  6.1,  8.3, 10.6, 12.8, 14.4, 16.1, 17.2, 18.3, 17.8, 17.8, 16.7, 15,   13.3, 12.2, 11.1, 10,    9.4,
                8.9,   8.3,  7.8,  7.2,  7.2,  6.7,  7.2,  8.3, 10,   12.2, 14.4, 16.7, 17.8, 18.9, 20,   20,   19.4, 18.3, 16.7, 15,   13.3, 12.8, 11.7, 10.6,
                10,    9.4,  9.4,  8.9,  8.3,  8.3,  8.9, 10,   11.7, 14.4, 17.2, 19.4, 21.1, 22.2, 23.3, 23.3, 22.8, 21.7, 20,   18.3, 16.7, 16.1, 15,   14.4,
                13.3, 13.3, 12.8, 12.2, 11.7, 11.7, 12.2, 12.8, 14.4, 16.7, 18.9, 21.1, 22.2, 23.3, 23.9
            } ) );

        private static readonly Forecast IntervalForecast = new Forecast(
            new Coordinate( 35.69, 139.69 ), "Tokyo, Japan",
            new DateTimeOffset( 2013, 07, 21, 6, 0, 0, 0, TimeSpan.FromMinutes( 540 ) ), 3,
            new RawDataValues( new double[] {
             //  0   3   6   9  12  15  18  21
                        20, 23, 26, 28, 26, 24,
                22, 22, 22, 23, 26, 27, 26, 25,
                24, 24
            } ) );

        private static readonly Forecast WindForecast = new Forecast(
            new Coordinate( 30.27, -97.74 ), "Austin TX",
            new DateTimeOffset( 2012, 02, 19, 7, 0, 0, 0, TimeSpan.FromHours( -6 ) ), 1,
            new Dictionary<ForecastData, RawDataValues> {
                { ForecastData.WindSustainedSpeed, new RawDataValues( new[] {
                                                         5.6,  6.1,  8.3, 10.6, 12.8, 14.4, 16.1, 17.2, 18.3, 17.8, 17.8, 16.7, 15,   13.3, 12.2, 11.1, 10,    9.4,
                    8.9,   8.3,  7.8,  7.2,  7.2,  6.7,  7.2,  8.3, 10,   12.2, 14.4, 16.7, 17.8, 18.9, 20,   20,   19.4, 18.3, 16.7, 15,   13.3, 12.8, 11.7, 10.6,
                    10,    9.4,  9.4,  8.9,  8.3,  8.3,  8.9, 10,   11.7, 14.4, 17.2, 19.4, 21.1, 22.2, 23.3, 23.3, 22.8, 21.7, 20,   18.3, 16.7, 16.1, 15,   14.4,
                    13.3, 13.3, 12.8, 12.2, 11.7, 11.7, 12.2, 12.8, 14.4, 16.7, 18.9, 21.1, 22.2, 23.3, 23.9
                } ) }
            } );

        private static readonly Forecast PressureForecast = new Forecast(
            new Coordinate( 30.27, -97.74 ), "Austin TX",
            new DateTimeOffset( 2012, 02, 19, 7, 0, 0, 0, TimeSpan.FromHours( -6 ) ), 1,
            new Dictionary<ForecastData, RawDataValues> {
                { ForecastData.Pressure, new RawDataValues( new[] {
                                                                    1005.6, 1006.1, 1008.3, 1010.6, 1012.8, 1014.4, 1016.1, 1017.2, 1018.3, 1017.8, 1017.8, 1016.7, 1015,   1013.3, 1012.2, 1011.1, 1010,   1009.4,
                    1008.9, 1008.3, 1007.8, 1007.2, 1007.2, 1006.7, 1007.2, 1008.3, 1010,   1012.2, 1014.4, 1016.7, 1017.8, 1018.9, 1020,   1020,   1019.4, 1018.3, 1016.7, 1015,   1013.3, 1012.8, 1011.7, 1010.6,
                    1010,   1009.4, 1009.4, 1008.9, 1008.3, 1008.3, 1008.9, 1010,   1011.7, 1014.4, 1017.2, 1019.4, 1021.1, 1022.2, 1023.3, 1023.3, 1022.8, 1021.7, 1020,   1018.3, 1016.7, 1016.1, 1015,   1014.4,
                    1013.3, 1013.3, 1012.8, 1012.2, 1011.7, 1011.7, 1012.2, 1012.8, 1014.4, 1016.7, 1018.9, 1021.1, 1022.2, 1023.3, 1023.9
                } ) }
            } );


        [Theory]
        [InlineData( Units.Metric, "C" )]
        [InlineData( Units.Imperial, "F" )]
        public void GetTemperatureSymbol_GivenUnit_ReturnsExpectedSymbol( Units units, string expected ) {
            string actual = ForecastExtensions.GetTemperatureSymbol( units );

            Assert.Equal( expected, actual );
        }


        [Theory]
        [InlineData( Units.Metric, "hPa" )]
        [InlineData( Units.Imperial, "inHg" )]
        public void GetPressureSymbol_GivenUnit_ReturnsExpectedSymbol( Units units, string expected ) {
            string actual = ForecastExtensions.GetPressureSymbol( units );

            Assert.Equal( expected, actual );
        }


        [Theory]
        [InlineData( Units.Metric, ForecastDisplayMode.Hour, "3027_-9774_M_H" )]
        [InlineData( Units.Imperial, ForecastDisplayMode.Day, "3027_-9774_I_D" )]
        public void GetTileName_GivenValidValues_ReturnsExpectedName( Units units, ForecastDisplayMode mode, string expected ) {
            Coordinate location = DefaultForecast.Location;

            string actual = ForecastExtensions.GetTileName( location, units, mode );

            Assert.Equal( expected, actual );
        }


        [Fact]
        public void GetDateOffset_GivenValueValue_ReturnsDate( ) {
            var expected = new DateTimeOffset( 1111, 11, 11, 0, 0, 0, TimeSpan.FromHours( 1 ) );
            var value = new DateTimeOffset( 1111, 11, 11, 11, 11, 11, TimeSpan.FromHours( 1 ) );

            var actual = ForecastExtensions.GetDateOffset( value );

            Assert.Equal( expected, actual );
        }

        [Theory]
        [InlineData( 0 )]
        [InlineData( 5 )]
        [InlineData( -6 )]
        [InlineData( 12.3 )]
        public void HoursFrom_GivenValidValues_ReturnsHourDifference( double expected ) {
            var second = DateTimeOffset.Now;
            var first = second.AddHours( expected );

            double actual = ForecastExtensions.HoursFrom( first, second );

            Assert.Equal( expected, actual, 2 );
        }

        [Theory]
        [InlineData( true, 2012, 02, 22, 16 )]
        [InlineData( false, 2013, 07, 23, 6 )]
        public void End_GivenValidForecast_ReturnsLastDate( bool defaultForecast, int year, int month, int day, int hour ) {
            Forecast forecast = defaultForecast ? DefaultForecast : IntervalForecast;
            DateTimeOffset expected = new DateTimeOffset( year, month, day, hour, 0, 0, forecast.Start.Offset );

            DateTimeOffset actual = ForecastExtensions.End( forecast );

            Assert.Equal( expected, actual );
        }


        [Fact]
        public void GetInterpolatedValue_CallsConvertMethod( ) {
            double expected = double.NaN;
            var values = new DataValues( new[] { 0.0, 1.0, 2.0 }, 1 );

            double actual = ForecastExtensions.GetInterpolatedValue( values, 1.2, _ => expected );

            Assert.Equal( expected, actual );
        }

        [Theory]
        [InlineData( 1, 0.00 )]
        [InlineData( 1, 0.50 )]
        [InlineData( 1, 1.00 )]
        [InlineData( 1, 1.23 )]
        [InlineData( 1, 1.64 )]
        [InlineData( 1, 2.05 )]
        [InlineData( 3, 0.00 )]
        [InlineData( 3, 0.50 )]
        [InlineData( 3, 1.00 )]
        [InlineData( 3, 1.23 )]
        [InlineData( 3, 1.64 )]
        [InlineData( 3, 9.99 )]
        public void GetInterpolatedValue_GivenValidValues_ReturnsInterpolatedValue( int interval, double offset ) {
            double expected = Math.Min( 2.0 * interval, offset ) * 50 / interval;
            var values = new DataValues( new[] { 0.0, 50.0, 100.0 }, interval );

            double actual = ForecastExtensions.GetInterpolatedValue( values, offset, v => v );

            Assert.Equal( expected, actual, 2 );
        }

        [Theory]
        [InlineData( 0.00, 0.00 )]
        [InlineData( 0.50, 0.25 )]
        [InlineData( 1.00, 0.50 )]
        [InlineData( 1.23, 0.27 )]
        [InlineData( 1.50, 0.00 )]
        [InlineData( 1.64, 4.86 )]
        [InlineData( 1.75, 4.75 )]
        [InlineData( 2.34, 4.67 )]
        [InlineData( 3.45, 0.54 )]
        public void GetInterpolatedValue_WithModulus_GivenValidValues_ReturnsInterpolatedValue( double offset, double expected ) {
            var values = new DataValues( new[] { 0.0, 0.5, 4.5, 0.0, 1.2 }, 1 );

            double actual = ForecastExtensions.GetInterpolatedValue( values, offset, v => v, 5.0 );

            Assert.Equal( expected, actual, 2 );
        }

        [Fact]
        public void GetInterpolatedValue_GivenEmptyCollection_ReturnsZero( ) {
            double expected = 0.0;
            var values = new DataValues( new double[0], 1 );

            double actual = ForecastExtensions.GetInterpolatedValue( values, 3.4, v => v );

            Assert.Equal( expected, actual );
        }


        [Theory]
        [InlineData( Units.Metric )]
        [InlineData( Units.Imperial )]
        public void GetTemperatureValues_UpdatesCachedValues( Units units ) {
            DataValues cachedValues = null;

            DataValues expected = ForecastExtensions.GetTemperatureValues( DefaultForecast, units, ref cachedValues );

            Assert.NotNull( cachedValues );
            Assert.Same( expected, cachedValues );
        }

        [Theory]
        [InlineData( Units.Metric )]
        [InlineData( Units.Imperial )]
        public void GetTemperatureValues_CalledMultipleTimesWithSameArguments_ReturnsSameValues( Units units ) {
            DataValues cachedValues = null;
            DataValues expected = ForecastExtensions.GetTemperatureValues( DefaultForecast, units, ref cachedValues );

            DataValues actual = ForecastExtensions.GetTemperatureValues( DefaultForecast, units, ref cachedValues );

            Assert.Same( expected, actual );
        }

        [Fact]
        public void GetTemperatureValues_GivenMetric_ReturnsForecastValues( ) {
            var expected = DefaultForecast.Temperature;
            DataValues cachedValues = null;

            DataValues actual = ForecastExtensions.GetTemperatureValues( DefaultForecast, Units.Metric, ref cachedValues );

            Assert.Equal( expected, actual );
        }

        [Fact]
        public void GetTemperatureValues_GivenImperial_ReturnsConvertedForecastValues( ) {
            var expected = DefaultForecast.Temperature.Select( ConvertValue.ToFahrenheit ).ToArray( );
            DataValues cachedValues = null;

            DataValues actual = ForecastExtensions.GetTemperatureValues( DefaultForecast, Units.Imperial, ref cachedValues );

            Assert.Equal( expected, actual );
        }


        [Theory]
        [InlineData( Units.Metric )]
        [InlineData( Units.Imperial )]
        public void GetWindSpeedValues_UpdatesCachedValues( Units units ) {
            DataValues cachedValues = null;

            DataValues expected = ForecastExtensions.GetWindSpeedValues( WindForecast, units, ref cachedValues );

            Assert.NotNull( cachedValues );
            Assert.Same( expected, cachedValues );
        }

        [Theory]
        [InlineData( Units.Metric )]
        [InlineData( Units.Imperial )]
        public void GetWindSpeedValues_CalledMultipleTimesWithSameArguments_ReturnsSameValues( Units units ) {
            DataValues cachedValues = null;
            DataValues expected = ForecastExtensions.GetWindSpeedValues( WindForecast, units, ref cachedValues );

            DataValues actual = ForecastExtensions.GetWindSpeedValues( WindForecast, units, ref cachedValues );

            Assert.Same( expected, actual );
        }

        [Fact]
        public void GetWindSpeedValues_GivenMetric_ReturnsForecastValues( ) {
            var expected = WindForecast.WindSpeed;
            DataValues cachedValues = null;

            DataValues actual = ForecastExtensions.GetWindSpeedValues( WindForecast, Units.Metric, ref cachedValues );

            Assert.Equal( expected, actual );
        }

        [Fact]
        public void GetWindSpeedValues_GivenImperial_ReturnsConvertedForecastValues( ) {
            var expected = WindForecast.WindSpeed.Select( ConvertValue.ToMilesPerHour ).ToArray( );
            DataValues cachedValues = null;

            DataValues actual = ForecastExtensions.GetWindSpeedValues( WindForecast, Units.Imperial, ref cachedValues );

            Assert.Equal( expected, actual );
        }


        [Theory]
        [InlineData( Units.Metric )]
        [InlineData( Units.Imperial )]
        public void GetPressureValues_UpdatesCachedValues( Units units ) {
            DataValues cachedValues = null;

            DataValues expected = ForecastExtensions.GetPressureValues( PressureForecast, units, ref cachedValues );

            Assert.NotNull( cachedValues );
            Assert.Same( expected, cachedValues );
        }

        [Theory]
        [InlineData( Units.Metric )]
        [InlineData( Units.Imperial )]
        public void GetPressureValues_CalledMultipleTimesWithSameArguments_ReturnsSameValues( Units units ) {
            DataValues cachedValues = null;
            DataValues expected = ForecastExtensions.GetPressureValues( PressureForecast, units, ref cachedValues );

            DataValues actual = ForecastExtensions.GetPressureValues( PressureForecast, units, ref cachedValues );

            Assert.Same( expected, actual );
        }

        [Fact]
        public void GetPressureValues_GivenMetric_ReturnsForecastValues( ) {
            var expected = PressureForecast.Pressure;
            DataValues cachedValues = null;

            DataValues actual = ForecastExtensions.GetPressureValues( PressureForecast, Units.Metric, ref cachedValues );

            Assert.Equal( expected, actual );
        }

        [Fact]
        public void GetPressureValues_GivenImperial_ReturnsConvertedForecastValues( ) {
            var expected = PressureForecast.Pressure.Select( ConvertValue.ToInchesOfMercury ).ToArray( );
            DataValues cachedValues = null;

            DataValues actual = ForecastExtensions.GetPressureValues( PressureForecast, Units.Imperial, ref cachedValues );

            Assert.Equal( expected, actual );
        }


        [Theory]
        [InlineData( -31, double.NaN, double.NaN )]
        [InlineData( -7, 5.6, 18.3 )]
        [InlineData( 0, 5.6, 18.3 )]
        [InlineData( 17, 6.7, 20.0 )]
        [InlineData( 41, 8.3, 23.3 )]
        [InlineData( 65, double.NaN, double.NaN )]
        [InlineData( 89, double.NaN, double.NaN )]
        public void TryGetExtremeValues_GivenOffset_ReturnsExpectedValues( double offset, double low, double high ) {
            var expected = double.IsNaN( low ) ? default( Pair<double, double>? ) : Pair.Create( low, high );
            DateTimeOffset date = DefaultForecast.Start.AddHours( offset );

            var actual = ForecastExtensions.TryGetExtremeValues( DefaultForecast.Temperature, DefaultForecast.Start, date );

            Assert.Equal( expected, actual );
        }

        [Theory]
        [InlineData( -30, double.NaN, double.NaN )]
        [InlineData( -6, 20.0, 28.0 )]
        [InlineData( 0, 20.0, 28.0 )]
        [InlineData( 18, 22.0, 27.0 )]
        [InlineData( 42, double.NaN, double.NaN )]
        [InlineData( 66, double.NaN, double.NaN )]
        public void TryGetExtremeValues_GivenOffsetAndInterval_ReturnsExpectedValues( double offset, double low, double high ) {
            var expected = double.IsNaN( low ) ? default( Pair<double, double>? ) : Pair.Create( low, high );
            DateTimeOffset date = IntervalForecast.Start.AddHours( offset );

            var actual = ForecastExtensions.TryGetExtremeValues( IntervalForecast.Temperature, IntervalForecast.Start, date );

            Assert.Equal( expected, actual );
        }

        [Fact]
        public void TryGetExtremeValues_GivenOffsetAndMinimumCount_ReturnsExpectedValues( ) {
            var expected = Pair.Create( 11.7, 23.9 );
            DateTimeOffset date = DefaultForecast.Start.AddHours( 65 );

            var actual = ForecastExtensions.TryGetExtremeValues( DefaultForecast.Temperature, DefaultForecast.Start, date, minimumCount: 0 );

            Assert.Equal( expected, actual );
        }

    }

}
